package terminal;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.SwingUtilities;

/** A simple terminal server library for programs that want to communicate
 * with remote clients via a text-based interface. Currently offers very basic
 * support for the telnet protocol. */
public class TerminalServer {
	
    /** the various supported terminal protocols */
    public enum TerminalProtocol {
        
        /** the telnet protocol */
        TELNET
    }
    
	/** the logger */
	private static final Logger log = 
	    Logger.getLogger(TerminalServer.class.getName());

	/** the current users */
	private final Set<TelnetTerminal> terminals;
	
	/** mapping of server socket channels to their associated protocols */
    private Map<ServerSocketChannel, TerminalProtocol> listenerProtocols;

    /** the multiplexer used to block on multiple listeners */
    private Selector selector;
	
	/**
	 * Constructs a new instance which will listen on the default
	 * @throws IOException if a network I/O exception occurs
	 */
	public TerminalServer() throws IOException {
	    
        terminals = new HashSet<>();
        listenerProtocols = new HashMap<>();
        selector = Selector.open();
	}

    /**
     * Opens a port for new connections using the given protocol
     * @param port the port on which the server listens for connections
     * @param protocol the protocol to use for connections that are initiated 
     * from this port
     * @throws IOException if a network I/O exception occurs
     */
    public void openPort(int port, TerminalProtocol protocol) throws IOException {
        
	    try {
	        ServerSocketChannel listener = ServerSocketChannel.open();
	        listener.bind(new InetSocketAddress(port));
	        listener.configureBlocking(false);
			listenerProtocols.put(listener, protocol);
			listener.register(selector, SelectionKey.OP_ACCEPT);
		} catch (IOException e) {
			log.log(Level.SEVERE, "Socket creation error", e);
			throw e;
		}
    }
	
    /**
     * Closes the specified port
     * @param port the port to close
     */
    public void closePort(int port) {

        for (ServerSocketChannel listener : listenerProtocols.keySet()) {
            if (listener.socket().getLocalPort() == port) {
                try {
                    listener.keyFor(selector).cancel();
                    listener.close();
                    return;
                } catch (Exception e) {
                    log.log(Level.WARNING, 
                        "Exception while closing port " + port, e);
                }
            }
        }
        log.log(Level.WARNING, "Unable to close port " + port + " (not open).");
    }
    
	/**
     * Returns a Terminal to a new remote client, blocking until a connection is
     * made
	 * @return a new Terminal to the remote client
	 * @throws IOException if a network I/O exception occurs
	 */
    public Terminal accept() throws IOException {
		
        log.info("Awaiting new connection");
        
        for (;;) {
            selector.select();
            for (SelectionKey key : selector.selectedKeys()) {
                if (key.isAcceptable()) {
                    ServerSocketChannel listener = 
                        (ServerSocketChannel) key.channel();
                    SocketChannel socket = listener.accept();
//                    socket.configureBlocking(false);
                    return new TelnetTerminal(socket);
                }
            }
        }
	}

	/**
	 * Disconnects the given client
	 * @param terminal the client to disconnect
	 */
	public void disconnect(Terminal terminal) {
	    assert SwingUtilities.isEventDispatchThread();
	    
	    terminals.remove(terminal);
	    try {
            terminal.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        log.info("Disconnection: " + terminal);
	}
}
